/**
 * JSON-friendly union that mirrors what Pinia Colada can hash.
 */
export type JsonValue =
  | null
  | string
  | number
  | boolean
  | JsonValue[]
  | { [key: string]: JsonValue };

/**
 * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
 */
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
  if (
    value === undefined ||
    typeof value === 'function' ||
    typeof value === 'symbol'
  ) {
    return undefined;
  }
  if (typeof value === 'bigint') {
    return value.toString();
  }
  if (value instanceof Date) {
    return value.toISOString();
  }
  return value;
};

/**
 * Safely stringifies a value and parses it back into a JsonValue.
 */
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
  try {
    const json = JSON.stringify(input, queryKeyJsonReplacer);
    if (json === undefined) {
      return undefined;
    }
    return JSON.parse(json) as JsonValue;
  } catch {
    return undefined;
  }
};

/**
 * Detects plain objects (including objects with a null prototype).
 */
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
  if (value === null || typeof value !== 'object') {
    return false;
  }
  const prototype = Object.getPrototypeOf(value as object);
  return prototype === Object.prototype || prototype === null;
};

/**
 * Turns URLSearchParams into a sorted JSON object for deterministic keys.
 */
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
  const entries = Array.from(params.entries()).sort(([a], [b]) =>
    a.localeCompare(b),
  );
  const result: Record<string, JsonValue> = {};

  for (const [key, value] of entries) {
    const existing = result[key];
    if (existing === undefined) {
      result[key] = value;
      continue;
    }

    if (Array.isArray(existing)) {
      (existing as string[]).push(value);
    } else {
      result[key] = [existing, value];
    }
  }

  return result;
};

/**
 * Normalizes any accepted value into a JSON-friendly shape for query keys.
 */
export const serializeQueryKeyValue = (
  value: unknown,
): JsonValue | undefined => {
  if (value === null) {
    return null;
  }

  if (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'boolean'
  ) {
    return value;
  }

  if (
    value === undefined ||
    typeof value === 'function' ||
    typeof value === 'symbol'
  ) {
    return undefined;
  }

  if (typeof value === 'bigint') {
    return value.toString();
  }

  if (value instanceof Date) {
    return value.toISOString();
  }

  if (Array.isArray(value)) {
    return stringifyToJsonValue(value);
  }

  if (
    typeof URLSearchParams !== 'undefined' &&
    value instanceof URLSearchParams
  ) {
    return serializeSearchParams(value);
  }

  if (isPlainObject(value)) {
    return stringifyToJsonValue(value);
  }

  return undefined;
};
